package se.webbzon.boltzmann.loader;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public class LoaderUtility {
	
	// The tag used to include other resources
	private static final String resourceTag = "resources";
	private static final String includeTag = "include";

	// A hash table containing all loaders mapped to their categories 
	private Hashtable<String,Collection<Loader>> categories;
	
	// A collection of listeners
	private Collection<LoaderUtilityListener> listeners;
	
	/*============================================================
	Constructors
	============================================================*/
	
	/** Creates a new loader utility. **/
	public LoaderUtility() {
		categories = new Hashtable<String,Collection<Loader>>();
		listeners = new ArrayList<LoaderUtilityListener>();
	}
	
	/*============================================================
	Public Methods
	============================================================*/
	
	/** Adds a new listener to the loader utility. **/
	public boolean addListener(LoaderUtilityListener listener) {
		return listeners.add(listener);
	}
	
	/** Removes a listener from the loader utility. **/
	public boolean removeListener(LoaderUtilityListener listener) {
		return listeners.remove(listener);
	}
	
	/** Adds a loader to the loader utility. **/
	public void addLoader(Loader loader) {
		// Put the loader into the hash table mapped to resource categories
		String resourceCategory = loader.getCategory();
		Collection<Loader> cl = categories.get(resourceCategory);
		if (cl == null) {
			cl = new ArrayList<Loader>();
			categories.put(resourceCategory, cl);
		}
		cl.add(loader);
	}
	
	/** Removes a loader from the loader utility. **/
	public void removeLoader(Loader loader) {
		// Removes the loader from the hash table mapped to resource categories
		String resourceCategory = loader.getCategory();
		Collection<Loader> cl = categories.get(resourceCategory);
		if (cl != null) 
			cl.remove(loader);
		if (cl.isEmpty())
			categories.remove(resourceCategory);
	}
	
	/** Loads all the resources by using an URL to a XML-file containing
	 * the resource information. **/
	public void loadFromXML(URL xmlURL) throws LoaderException {
		xmlRead(xmlURL,xmlURL);
	}
	
	/*============================================================
	Private Methods
	============================================================*/
	
	/** Notifies all listeners that a loading of a XML resource has began. **/
	private void notifyOnLoadBegin(LoaderUtilityPointer pointer) {
		for (LoaderUtilityListener listener : listeners)
			listener.onLoadBegin(this,pointer);
	}
	
	/** Notifies all listeners that a loading of a XML resource has ended. **/
	private void notifyOnLoadEnd(LoaderUtilityPointer pointer) {
		for (LoaderUtilityListener listener : listeners)
			listener.onLoadEnd(this, pointer);
	}
	
	/** Reads all the resources from a XML resource file. **/
	private void xmlRead(URL rootURL, URL xmlURL) throws LoaderException {
		final LoaderUtilityPointer pointer = new LoaderUtilityPointer(rootURL,xmlURL);
		notifyOnLoadBegin(pointer);
		try {
			DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
			DocumentBuilder db = dbf.newDocumentBuilder();
			Document xmlDoc = db.parse(xmlURL.openStream());
			
			// Retrieve the resource node
			Node resources = xmlDoc.getChildNodes().item(0);
			
			if (resources.getNodeName().equals(resourceTag))
				// Parse all the categories in this XML resource
				xmlParseCategories(pointer,resources.getChildNodes());
			else
				throw new LoaderException("XML resources must start the tag <resources>");
		} catch (	ParserConfigurationException | 
					SAXException | 
					IOException e) {
			throw new LoaderException(e);
		} finally {
			notifyOnLoadEnd(pointer);
		}
	}
	
	/** Traverses a XML node list in search for categories. **/
	private void xmlParseCategories(LoaderUtilityPointer property, NodeList root) 
			throws LoaderException {
		for (int i = 0; i < root.getLength(); i++) {
			Node category = root.item(i);
			String categoryName = category.getNodeName();
			if (categoryName.equals(includeTag)) {
				// Include another XML resource
				Node include = category.getChildNodes().item(0);
				try {
					URL xmlURL = new URL(property.getXMLURL(),include.getNodeValue());
					xmlRead(property.getRootURL(),xmlURL);
				} catch (MalformedURLException e) {
					throw new LoaderException(e);
				}
				
			} else if (category.hasChildNodes() && categories.containsKey(categoryName)) {
				// Parse all the resources in the category
				xmlParseResources(	property,
									category.getChildNodes(),
									categories.get(categoryName));
				
			}
		}
	}
	
	/** Traverses a XML node list in search for resources. **/
	private void xmlParseResources(	LoaderUtilityPointer property, 
									NodeList categoryContent, 
									Collection<Loader> loaders) throws LoaderException {
		for (int i = 0; i < categoryContent.getLength(); i++) {
			Node resource = categoryContent.item(i);
			if (resource.hasChildNodes()) {
				String resourceName = resource.getNodeName();
				// Create a subset of all the loaders
				ArrayDeque<Loader> loaderSubset = new ArrayDeque<Loader>();
				for (Loader loader : loaders)
					if (loader.getSupportedResources().contains(resourceName))
						loaderSubset.add(loader);
				// Check if any loader is in the subset
				if (!loaderSubset.isEmpty())
					xmlParseResourceAttributes(	property,
												resource.getChildNodes(),
												loaderSubset,
												resourceName);
			}
		}
	}
	
	/** Traverses a XML node list in search for resource attributes. **/
	private void xmlParseResourceAttributes(LoaderUtilityPointer property, 
											NodeList resourceContent, 
											Collection<Loader> loaders,
											String resourceName) 
													throws LoaderException {
		HashMap<String,String> attributes = new HashMap<String,String>();
		// Read all the attributes from the resource
		for (int i = 0; i < resourceContent.getLength(); i++) {
			Node resourceData = resourceContent.item(i);
			if (resourceData.hasChildNodes()) {
				attributes.put(	resourceData.getNodeName(),
								resourceData.getChildNodes().item(0).getNodeValue());
			}
		}
		
		// Protect the map by using an unmodifiable map
		Map<String,String> unmodifiableAttributes = 
				Collections.unmodifiableMap(attributes);
		for (Loader loader : loaders)
			loader.load(property, resourceName, unmodifiableAttributes);
	};
	
}
